refactor(clone): type-driven clone defaults with honor-or-throw decorators#3040
Conversation
…ainer elements - Add `@defaultCloneMode(mode)` class decorator for declaring how instances of a type should be cloned when encountered as field values - Change container element cloning: array elements now use `undefined` as cloneMode so each element's type-level `_defaultCloneMode` is consulted. Unknown types (no _defaultCloneMode) default to Assignment (shared reference) instead of Deep, preventing crash on DOM/native objects. - Add `_defaultCloneMode` to ICustomClone interface - Mark RenderState system classes with @defaultCloneMode(Deep): DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe clone system now uses class-level default clone modes, clone-map-based identity tracking, and updated resource/reference-count handling across core engine types. Particle, physics, UI, and post-process components were adjusted to the new clone behavior, and the clone-related tests and docs were expanded accordingly. ChangesClone pipeline and core remapping
Clone-mode updates across engine, particle, physics, and UI classes
Clone and ref-count test coverage
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/clone/CloneManager.ts`:
- Around line 67-69: The defaultCloneMode decorator accepts any CloneMode value,
but the runtime logic at lines 141-149 only properly handles Deep and Shallow
modes, while Ignore is checked separately earlier at line 138. To fix this,
modify the defaultCloneMode function signature to restrict the mode parameter to
only accept CloneMode.Deep or CloneMode.Shallow (using a union type or
overload), preventing callers from incorrectly decorating with CloneMode.Ignore
which would not be honored at runtime.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 585fad6c-db8f-4c53-9512-ff3e147e9dde
📒 Files selected for processing (8)
packages/core/src/clone/CloneManager.tspackages/core/src/clone/ComponentCloner.tspackages/core/src/shader/state/BlendState.tspackages/core/src/shader/state/DepthState.tspackages/core/src/shader/state/RasterState.tspackages/core/src/shader/state/RenderState.tspackages/core/src/shader/state/RenderTargetBlendState.tspackages/core/src/shader/state/StencilState.ts
…+ type-driven deep clone Core changes: - Clone is now opt-out: all enumerable fields are cloned unless @ignoreClone - Default clone mode for unknown types is Assignment (shared reference) — safe for DOM elements, native handles, and any unrecognized constructor - Types that need independent copies declare @defaultCloneMode(CloneMode.Deep) - Identity-map based deep clone with cycle/shared-subgraph dedup - 3-stage lifecycle: Construct → Populate (copyFrom or for...in) → Finalize (_cloneTo) - Container elements (Array/Map/Set) go through the type-driven gate individually - @deepClone/@assignmentClone/@shallowClone kept as no-op for backward compat - @ignoreClone remains functional RenderState classes marked @defaultCloneMode(Deep): DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eep) Particle system: ParticleGeneratorModule (+ all subclasses via inheritance), MainModule, BaseShape (+ all shape subclasses), ParticleGenerator, ParticleCompositeCurve, ParticleCurve, CurveKey, ParticleCompositeGradient, ParticleGradient, GradientColorKey, GradientAlphaKey, Burst Post-process: PostProcessEffect (+ BloomEffect, TonemappingEffect via inheritance), PostProcessEffectParameter (+ Float/Bool/Color/Vector/Enum/Texture) Physics: JointLimits, JointMotor Other: VirtualCamera, Skin Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mark Entity/Component with @defaultCloneMode(Remap) and split Entity.clone() into a register-then-copy two-pass: pre-register every source Entity/Component to its clone in the identity map across the whole subtree, so references nested in arrays/maps/objects are remapped through the clone gate, not just top-level fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Re-register @deepClone/@assignmentClone/@ignoreClone as field-level modes (highest priority). - Containers (Array/Map/Set/TypedArray/plain object) default to deep clone. - Type defaults: ReferResource -> Assignment, math types -> Deep, UpdateFlagManager -> Ignore. - Thread srcRoot/targetRoot through the gate so Signal._cloneTo can remap listeners. - Add CloneMode.Ignore; drop erroneous @deepClone on Joint limits/motor _updateFlagManager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efaults - Remove @deepClone/@assignmentClone/@shallowClone field decorators (171) across core & ui. - Fields resolve via container default deep + type-level @defaultCloneMode + Assignment fallback. - Mark ShaderData & Signal Deep; ShaderData adds _cloneTo delegating to cloneTo. - Clean up now-unused clone decorator imports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t-assignment # Conflicts: # packages/core/src/particle/ParticleGenerator.ts
…l run Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev/2.0 #3040 +/- ##
===========================================
+ Coverage 85.22% 85.40% +0.18%
===========================================
Files 810 811 +1
Lines 94673 94654 -19
Branches 11342 11507 +165
===========================================
+ Hits 80684 80839 +155
+ Misses 13899 13725 -174
Partials 90 90
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…low) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…remap via identity map - Assignment is now a plain reference share: the gate never touches refCount. Ref-count ownership belongs to each class's own logic (_cloneTo hooks / setters), balanced by that class's destroy path. Fixes double-counting on Camera/Animator/AudioSource clones, Shader leak via _replacementShader, and permanent leaks for user-script-held resources. - Field decorators are highest priority at all depths: the ComponentCloner top-level remap special case is removed; Entity/Component remap unified through the pre-populated cloneMap (O(1)); CloneUtils path-walk deleted. @deepClone on an Entity/Component ref warns and falls back to remap. - Entity.clone() builds the tree and registers identity pairs in one walk; srcRoot/targetRoot threading removed — _cloneTo hooks receive cloneMap. - Container classification centralized (single _isContainer); DataView clones as bytes (was: crash via generic object branch); functions in containers are shared instead of dropped to undefined, while constructor-rebound top-level handlers keep the clone's own binding. - Register ColliderShape / ui Transition as @defaultCloneMode(Deep) so cloned colliders/interactives own independent instances; restore @ignoreClone + setter pattern for TextRenderer/Text fonts and MeshColliderShape mesh; SpriteTransition acquires state-sprite refs on clone. - shallowClone decorator warns on use; dead copyProperty/copyProperties removed; CloneMode exported; orphan imports cleaned. - Tests: decorator-priority semantics updated; new regressions for function fields, DataView, refCount balance (RT/controller/font/sprite-transition), collider-shape and transition instance independence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve tests/src/core/physics/ColliderShape.test.ts: physics-lite describe removed upstream; move the shape-independence regression into the PhysX describe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed a follow-up that reworks two load-bearing decisions of this PR (design discussion happened offline): 1. The clone gate no longer touches refCount. 2. Remap is unified through the identity map and field decorators win at every depth. The Also in this push: Known pre-existing (not addressed): Tests: core 907+ / physics / ui+loader all green locally; new regressions cover function fields, DataView, refCount balance (RT / controller / font / sprite-transition / script-held textures), and instance independence for collider shapes & transitions. Branch merged with latest 🤖 Generated with Claude Code |
…eleases Component top-level fields sharing a registered ref-counted resource (ReferResource family) acquire one reference at the clone gate (+1, and -1 when replacing an owned preset); the owning component's destroy path releases it (implementation contract). Scripts have no per-field destroy logic, so gate acquisitions are recorded in a ledger and released in Script._onDestroy. Below the top level the gate never counts: container elements and plain-object fields are plain shares, and nested classes pair the acquisition themselves (SpriteTransition._cloneTo +1 paired with Transition.destroy -1, hoisted to the base class so future ReferResource-valued transitions inherit the release). Manual +1 compensations in Camera/Animator/AudioSource._cloneTo are removed (the gate acquisition replaces them); TextRenderer/Text._font return to gate accounting. The counted-resource test is explicit registration (@defaultCloneMode(Assignment)), excluding duck-typed counters like Shader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leak Add slot-ownership contract suites asserting the full lifecycle (baseline → clone +1 → setter churn → destroy release) for every counted component slot: Camera.renderTarget, Animator.controller, AudioSource.clip, SpriteRenderer/SpriteMask/ui Image sprite, MeshRenderer.mesh, MeshColliderShape.mesh, SpriteTransition states, and the Script ledger (reassignment still releases the original). Add ShaderData/Material cascade suites: setTexture swap/clear, doubly-referenced hosts (±refCount propagation), renderer clone balance, and instance-material lifecycles. The new suites exposed a pre-existing leak: destroying a ParticleRenderer never released the MeshShape's mesh (+1 in the mesh setter, no release path). Add BaseShape._destroy, called from EmissionModule._destroy, with MeshShape releasing through its setter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shallow is no longer a distinct mode; keeping the decorator silently behaving as deep clone hides the semantic change. Migrate with @deepClone (independent copy) or @assignmentClone (share the reference). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Public API (getFieldModes, deepCloneObject) first, @internal entries (_registerFieldMode, _cloneValue) after, private helpers last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Script fields are user territory: the engine cannot pair a release for an automatic acquisition (users can't know which slots were acquired), so the gate does no counting for script slots at all — replacing the clone-acquisition ledger. Users who need gc protection manage counts themselves, matching the pre-2.0 semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cloning's only ref-count job is acquiring the count for the slot it copies. Releasing on destroy is the owning class's existing contract: engine components in their destroy paths, script authors in onDestroy. Remove the script exemption flag; Script.ts is untouched by this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_cloneValue is pure cloning again (4 params, Assignment = plain share). ComponentCloner — the only place slots own references — detects the share (result === source value, registered resource only; remapped/deep results never match) and acquires through CloneManager._acquireSlotOwnership. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Not a priority rule: deep-copying an Entity/Component is an unexecutable directive, so it recovers to remap with a warning. Every executable decorator still wins over the type default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the model summary and the ref-count contract in two short paragraphs; per-type behavior and deep-clone stages live on the methods themselves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…shared clone [vec3, vec3, vec3] must clone into one NEW Vector3 referenced three times (identity-map dedup), not three copies and not the source instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last hole in honor-or-throw: pointing the decorator at a function silently shared the source's binding. Code is not a cloneable graph, so the assert now rejects it like entities, assets, and runtime state; a function reached by a propagated deep intent inside the subtree still shares, unthrown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GuoLei1990
left a comment
There was a problem hiding this comment.
增量审查 @ c61e0684(第四十一轮)— delta = 18 commit(真行为变更批次:@deepClone 语义改为深入子树 + for...in→Object.keys 修 clone 走原型方法 + engine-runtime 分支拆分 + _createCloneTarget 抽取 + CloneManager 停止 re-export + 一批注释/文档收敛)— ✅ 两处高风险行为变更经对抗验证 + 反向可证伪测试 + CI 12/12 全绿逐条坐实为安全(其中两处实为 correctness 修复),无 P0/P1,LGTM
总结
自上轮 0494dca5(我第四十轮 --comment LGTM 点)起 tip 线性推进 18 个 commit(compare 报 ahead:18 / behind:0、merge_base=0494dca56、status ahead,无 force-push;git ls-remote refs/pull/3040/head 校准真 tip=c61e0684e4c9befb688c743be02c6e11b64d6e8f 与 gh pr view headRefOid 一致)。与第 31~40 轮的纯注释 delta 不同,本批含多处真行为变更,逐一深审如下。碰 20 文件,核心是 CloneUtil.ts(+110/-106) + Signal.ts + 365 行新测。CI build×3 + e2e×4 + codecov×3 + lint + labeler = 12/12 全绿。
1. for...in → Object.keys(perf 702d1a79)—— 逐链核实为 correctness 修复,非数据丢失
_deepCloneObject / ComponentCloner.cloneComponent 的字段遍历从 for (const k in source)(含继承的可枚举属性)改为 Object.keys(source)(仅自有可枚举)。commit body 论断"release bundle 用 swc loose+es5 downlevel,把原型方法 emit 成可枚举的 prototype.name = fn,故 for...in 把原型方法当字段拷成 clone 自有属性(cloned MeshRenderer 61 own keys vs fresh 31,ShaderData 41 vs 6)"。对抗验证坐实(mechanism-level):
- 全
packages/**/*.ts零enumerable: true、零原型数据赋值(唯一的prototype.*=是Polyfill.ts里 feature-gated 的函数 polyfill)。 - 所有实例数据 = class field(
_foo = value),swc emit 为 ctor 内this._foo=(自有属性)→Object.keys全捕获,零丢失。核了Renderer/MeshRenderer/Transform/ShaderData/Component/EngineObject/DisorderedArray/SafeLoopArray全部字段。 - 继承的可枚举属性 100% 是 loose-mode 原型方法 +
constructor(clone 本就不该拷成 own props)。 _fieldModes(Object.defineProperty无enumerable→ 默认 false)、class getter(accessorenumerable:false)both 遍历都不走,无变化。- Math 值类型走
copyFrom分支绕过 field-walk,原型可枚举性无关。
结论:Object.keys 恰好丢弃且仅丢弃 loose-mode 原型方法 —— clone 不再携带原型方法作为 own props,是修复而非回归。e2e 克隆真实 prefab 全绿佐证无数据字段丢失。
2. @deepClone 深入子树(feat 64410a6a)—— 无生产回归,Signal shield 正确必要
语义变更:@deepClone 的深拷意图从"止于装饰字段(容器得空壳持共享成员)"改为"传播进成员"(forceDeepClone 经 _deepCloneObject 线程进每个子 _cloneValue),子树内"软"处理(asset 共享 / entity remap / runtime state 保 clone 自有 / function 共享不 throw);honor-or-throw 移入 _cloneValue→_assertDeepCloneable,只作用于装饰值本身,不作用于传播的子节点。commit body 框为恢复 pre-#1682 的递归行为(#1682 rewrite 意外改成 stops-at-field)。对抗验证坐实:
- 零生产回归可能:
@deepClone在一方 engine 源码packages/*/src零使用(仅测试 fixture)。forceDeepClone生产路径永不为 true,故传播变更不可能回归任何现有一方消费者 —— 这是给三方/编辑器代码的 latent-correctness 变更。 Signal._listeners @ignoreCloneshield 正确必要:逐链 trace —— 无 shield 时传播的@deepClone到 Signal(extends DataObject→field-walk)会走SafeLoopArray分支深拷 listener 对象,再_cloneTo在其上 rebuild = 双份 state;@ignoreClone(本 PR 新加Signal.ts:13)跳过 walk 精准封死,配:1044回归测试。- 无其它未 shield 的 SafeLoopArray/DisorderedArray:穷举所有此类字段 +
_cloneTohook,Signal._listeners是唯一可被传播深拷意图作为成员触达的 value-type 数组(已 shield);其余全在 Entity/Component/managers(remap 或永不 field-walk)。无遗留双份 state bug。
反向可证伪测试覆盖完整:@deepClone subtree propagation(plain-class 成员深拷 / force 穿透嵌套 plain object / engine-bound 成员保默认)、function inside subtree shared、@deepClone overrides default on plain container(DisorderedArray force field-walk)、@deepClone on UpdateFlagManager throws、BagHolderScript(force 仍尊重 Bag 自己的 @ignoreClone)。
3. engine-runtime 分支拆分(fix 36472b39)—— 语义分类修正
base 把 UpdateFlagManager|UpdateFlag|DisorderedArray|SafeLoopArray 混为一个"engine runtime state"分支(force 全 throw)。tip 拆分:UpdateFlagManager|UpdateFlag→return preset(不可 override,flag/manager 互持、field copy 解不了任一侧,throw 在 _assertDeepCloneable);DisorderedArray|SafeLoopArray→独立分支(plain container,force 时 field-walk 正确深拷)。分类正确 —— 前者是真正的 transient 运行时耦合,后者只是 plain container。测试逐一坐实(undecorated 保 clone 自有 / forced 深拷 / @deepClone on UpdateFlagManager throws)。
4. _createCloneTarget 抽取(refactor 3d06a6d2/729573550)+ fold-branch(b0f8226f)—— 等价、cycle-safe 保持
_createCloneTarget 解析目标实例(复用 preset 或 bare-construct)并在返回前 cloneMap.set(source, dst)(:150),identity lookup 留在 call site:已克隆对象经 call-site 的 cloneMap.get 早退,不重放 populate + _cloneTo hook(3d06a6d2 body 明说 hook 会 acquire refs + register listeners,重放静默翻倍,配新测 pin)。cycle-safety lookup → set → recurse 逐分支保持。
5. CloneManager 停止 re-export(fix 983fe599)—— 真跨包 .d.ts correctness 修复
CloneManager 类级 @internal + core tsconfig stripInternal:true → emit 的 .d.ts 抹除该类,index.ts 的 export { CloneManager, ... } 悬空 = TS2305(无 skipLibCheck 的消费者)。只有三个装饰器是 public API,一方零消费者 import 该类。与第 37 轮 DataObject 同型的真修复,commit body 准确。
6. 一批注释/文档收敛 + ShaderData._addTexturesReferCount static→instance(a1eb05c8)—— 等价
ShaderData._addTexturesReferCount:body 只碰property._addReferCount(零 static state),两 caller 均实例方法 → static→instance 等价,commit body 准确。CloneUtil.ts28→0 注释、SkinnedMeshRenderer/MeshColliderShape/Transition/BaseShape/MeshShape/ParticleCompositeGradient删"why"注释:这些非任何外部fix:的记录依据(joint-texture 行是本 PR 自己e5894ae42/d09581217引入,非外部 fix)。关键:这批注释描述的 refcount / cycle-safety / alias-guard invariant 现全部由反向可证伪测试守护 —— 新增RefCountContract.test.ts(266 行,8 组件 clone acquire/churn/release,含MeshRenderer.mesh/MeshShape.mesh_cloneTo路径) +ShaderDataRefCount.test.ts(texture cascade + cloneTo-into-referenced displacement) +CloneManager.test.ts的 aliasing/cycle/binary-preset 测试。故注释虽删,invariant 未成为静默 delete-hazard(与第 38 轮判据一致)。- 文档
clone.mdxen/zh:a4dc14b2补 function-value 行、Map key cloning、widen throw list、narrow "any depth",逐条对代码核准确;dbb7624c修 ref-counting 指引(旧文档让 script 作者在onDestroy释放 clone 加的 asset 引用,但 public Script surface 做不到 → 改指向 asset 自身destroy(),真 aspirational-doc 修复);deep-default 双入口文档(DataObject / copyFrom + 免参构造)对CloneUtil.ts:75/84准确。
问题
- [P3]
packages/core/src/Signal.ts:12— 新增单行//注释// Rebuilt by \_cloneTo`; must survive even a propagated @deepClone.末尾带句号。baseSignal.ts无既有单行//惯例可循,按项目单行//` 规则不加句号。纯风格 nit,非阻塞。 - [P3]
docs/*/core/clone.mdx:154-155—bacause拼写(en/zh 各 2 处)。pre-existing,但本 PR 的 prettier commit8a954232恰好重排了这两行却没顺手改。非阻塞,建议顺手修。
注释合规
- 本 delta 新增 7 处单行
//:1 处Signal.ts(上述 P3)+ 6 处测试文件。测试的 6 处全是多行//块(末行带句号),跟随本文件既有惯例(CloneManager.test.ts大量同形态多行块),合规。 CloneUtil.ts保留的唯一注释是类级 JSDoc(多行/** */带句号),合规。- 无其它新增违规。
已核对为「已解决 / 不适用 / 不重提」
- [P0]
UpdateFlagManagerObject→object—— 第 29 轮修复闭环,本 delta 未触碰,守住。 - [P3]
CloneManager.test.tsstale fixture 注释 —— 第 35 轮闭环。 - [P2]
Entity._createCloneEntity两遍克隆 block 注释(第 39 轮) —— 本 delta 未触碰Entity.ts,状态不变,非本 delta scope,不重提。 - 历史全部闭环项(type-driven gate 核心、dedup 下沉、engine-bound throw、
_cloneTo删除 + 摘@ignoreClone、DataObject 类级@internal删除、order-dependent dedup bug、function 分支归位、unexport CloneUtil、注释陈述化等)—— 本 delta 的for...in→Object.keys与@deepClone传播经对抗验证安全,全部守住。 - 我第 29 轮 latest state-changing review =
APPROVED @ 55f0009af,门控已清;reviewDecision=REVIEW_REQUIRED是 repo 要求他人 owner 审、非我门控。GuoLei1990 早前CHANGES_REQUESTED @ aea87ef9d(2026-07-17)即我本人,已被我后续APPROVED @ 55f0009af(2026-07-18)取代。 - 自上轮以来无新 PR 评论 / inline / 他人 review 需 reconcile。
判例(第四十一轮)
- 含真行为变更的大 delta(18 commit)必按"高风险变更逐条对抗验证"审,不能因前 40 轮都是纯注释就放松:本批的
for...in→Object.keys(改核心 clone walk)与@deepClone传播(改装饰器语义)都是能静默改变克隆结果的变更,各派一个对抗 agent 穷举证伪(前者查全仓enumerable:true/原型数据 + downlevel 机制;后者查全仓@deepClone生产用法 + 所有 SafeLoopArray/DisorderedArray +_cloneTo交叉),比逐行读推演硬。 - "删 X 的可枚举遍历"型 perf 变更的验收 = 证明被丢弃集合恰好等于无害集合:
for...in→Object.keys丢继承可枚举属性,安全当且仅当"所有数据=自有 class field(Object.keys 捕获)+ 所有继承可枚举=方法(该丢)"。全仓 grepenumerable:true零命中 + downlevel 机制(swc loose emit 原型方法为可枚举)双证。 - "改装饰器传播语义"的回归面 = grep 该装饰器的全部生产用法:
@deepClone一方源码零使用 →forceDeepClone生产永不为 true → 传播变更零生产回归,是 latent-correctness 而非 active regression。同时穷举唯一能被传播意图触达的 value-type 容器(Signal._listeners)是否已 shield。 - 注释虽删但 invariant 由反向可证伪测试守护 ≠ 静默 delete-hazard:refcount/cycle/alias invariant 从"why 注释"迁移为
RefCountContract/ShaderDataRefCount测试断言,revert 任一 invariant 即测试红 —— 这是 invariant 的执行化保全,非静默移除守卫(与第 38 轮判据一致)。 - GitHub reviews API 默认页有 staleness,校 steady-state 必
--paginate:默认页只返回到 2026-07-17 的 review,漏掉我 rounds 29-40(2026-07-18/19),一度显示我最新 state-changing review 是CHANGES_REQUESTED而非真实的APPROVED @ 55f0009af。--paginate取全量 +compare校55f0009af/0494dca5确为 tip 祖先才还原真相。
简化建议
无 —— 这批 18 commit 是一次高质量的实质推进:@deepClone 恢复 pre-#1682 的子树递归语义并配 honor-or-throw 只作用于装饰值本身、for...in→Object.keys 修掉 clone 携带原型方法的 correctness bug(顺带 4x 提速:20-entity 树 0.389ms→0.094ms)、engine-runtime 分支按真实耦合正确拆分、CloneManager 停止 re-export 修跨包 .d.ts 悬空。两处高风险行为变更经对抗验证 + 365 行反向可证伪测试 + CI 12/12 全绿逐条坐实安全,其中两处实为 correctness 修复。仅剩两个 P3 风格/拼写 nit 非阻塞。本 PR 现已可合并。
GuoLei1990
left a comment
There was a problem hiding this comment.
🫧 尘小沫
增量审查 @ 7f5e8d66(第四十二轮)— delta = 2 commit(纯 TSDoc 收敛)— ⚠️ 执行体 byte-unchanged,1×P2 ownership 边界说明丢失,无 P0/P1
总结
自上轮 c61e0684 起,GitHub compare 为 ahead:2 / behind:0,merge base 正是 c61e0684,无 force-push。两次提交仅修改 DataObject.ts(+2/-3)与 CloneMode.ts(+4/-8)的 TSDoc;剥除注释后,两文件在 checkpoint 与目标 HEAD 的执行体哈希分别完全一致。CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿。
DataObject 的新文案更准确:CloneUtil 以 instanceof DataObject 选择默认 field-walk;只有不存在同构 compatible preset 时 _createCloneTarget 才裸构造,因此 “deep-cloned by default” 与 “when no compatible preset exists” 都对齐真实控制流,也保住了此前无参构造契约。
CloneMode 三个 member 的精简本身准确:Ignore 保留 target preset,Assignment 直接返回 source,Deep 递归传播且嵌套 engine-bound 值走默认分支。删掉 Assignment 注释里的 refCount 细节也是正确的 ownership 收敛——字段值策略由 CloneMode/CloneUtil 决定,component 顶层资源槽的 +1/-1 由 ComponentCloner._transferSlotOwnership 结算,公开 clone 文档仍保留完整资源契约。
问题
-
[P2]
packages/core/src/clone/enums/CloneMode.ts:2—Field clone behavior.把关键的 owner 边界压没了,而且只是复述 enum 名。CloneMode并不穷尽字段的 clone 状态:它只承载三个 decorator 的显式 override;未装饰字段以fieldMode === undefined进入CloneUtil._cloneByDefault,Entity/Component remap、ReferResource sharing、runtime preset、container/DataObject deep 等 type-driven default 都由后者权威决定。历史 commit9ee44e3046 refactor(clone): drop the unused CloneMode.Remap member的正文也明确记录了 “CloneMode now describes only what a field decorator can request”,当前改动删除了这条边界,容易让后续维护者误以为这里是完整状态机。建议保留CloneUtil作为默认策略唯一 owner,并将说明收敛为:/** * Field-level clone override registered by a clone decorator; undecorated fields use type-driven defaults. */
架构治理核对
上游唯一 writer 是 deepClone / assignmentClone / ignoreClone 对 _fieldModes 的注册;下游分别由 CloneUtil 执行值策略、ComponentCloner 结算顶层资源 ownership。此次没有新增重复状态、平行状态机、可推导持久状态或 legacy/compat 路径;唯一需要保留的是上述 override/default 的职责分界,而不是再增加同步层或枚举成员。
已核对为「已解决 / 不适用 / 不重提」
DataObject类级@internal导致声明被stripInternal抹除的问题保持闭环;本 delta 未改声明或导出链。- 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction 等行为修复全部守住;执行体哈希一致。
- 上轮已列的
Signal标点、clone.mdx拼写以及Entity两遍克隆注释均未被本 delta 触碰,不作为增量 finding 重复提出。 - 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。
简化建议
除恢复 CloneMode 的一句职责边界外,无进一步简化建议。该 P2 是维护者心智与协议归属问题,不阻塞合并;运行时行为没有变化。
GuoLei1990
left a comment
There was a problem hiding this comment.
🫧 尘小沫
增量审查 @ b10a97c5(第四十三轮)— delta = 1 commit(native shape 挂载 ownership 收口 + kinematic triangle-mesh clone 回归)— ❌ 1×P0:在 native actor 切到 kinematic 前挂载 triangle mesh,新增测试读取乐观镜像而假绿
总结
自上轮 7f5e8d66 起,GitHub compare 为 ahead:1 / behind:0,merge base 正是 7f5e8d66,无 force-push。本轮只含 b10a97c5 fix(physics): defer cloned shape attachment to collider sync,修改 4 个文件(+54/-49)。CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,实际 Vitest 为 121 files / 1633 tests 全过。
把 ColliderShape._collider 设为 @ignoreClone、删除 shape 侧 _attachToCollider / _detachFromCollider,并让 Collider._setNativeShapeAttached 成为 Core 唯一挂载入口,方向正确:owner backlink、native membership 和 _isShapeAttached 不再由 collider/shape 两套状态机竞争。但当前 clone 的 native actor 状态同步顺序违反了 PhysX 的挂载前置条件,而且 adapter/Core 的两层乐观镜像掩盖了失败。
问题
-
[P0]
packages/core/src/physics/Collider.ts:168— cloned kinematicDynamicCollider会在 native actor 仍为 non-kinematic 时尝试挂载 non-convex triangle mesh;PhysX 拒绝后,代码仍把两层镜像记为已挂载,导致 clone 静默失去真实碰撞形状。完整链路是确定的:
ComponentCloner直接写target[k],所以_isKinematic=true只复制到 TS 对象,没有经过 native setter;新加的_collider @ignoreClone又使MeshColliderShape在 field walk 中先重建为 detached native shape。随后DynamicCollider._cloneTo()调用target._syncNative(),但DynamicCollider.ts:487先执行super._syncNative(),因此这里的addShape()先发生;nativesetIsKinematic(this._isKinematic)直到DynamicCollider.ts:504才执行。对 simulation triangle mesh,
PxRigidActor::attachShape的契约要求PxRigidDynamic已是 kinematic;checked PhysX 会返回false,release 下也不能依赖违反前置条件的行为。更严重的是,packages/physics-physx/src/PhysXCollider.ts:32-35丢弃了attachShape()的布尔结果,仍无条件_shapes.push(shape);本 helper 又无条件写shape._isShapeAttached = true。之后虽然 actor 被切成 kinematic,却没有任何重试,于是 native actor 没有 shape,而 adapter_shapes与 Core_isShapeAttached都声称有。新测试
tests/src/core/physics/MeshColliderShape.test.ts:510只断言clonedCollider._nativeCollider._shapes.length === 1,读到的正是上述无条件 push 的 JS mirror,因此无法证明 native 挂载成功。请至少在DynamicCollider._syncNative()中先把 native actor 的 kinematic 状态写入,再调用super._syncNative(),并删除后面的重复setIsKinematic;同时让最终挂载结果成为状态写入依据(例如把ICollider.addShape/ PhysX adapter 的成功结果传回唯一 helper),测试改为断言实际 Px actor 的 shape 数或用 raycast/contact 验证碰撞。否则使用 checked/custom PhysX runtime 的合法 kinematic triangle-mesh clone 会稳定丢碰撞。
架构治理核对
应保留的唯一 Core owner 是 Collider:它拥有 _shapes、shape backlink 以及 attach/detach funnel;不要恢复 shape 侧 helper,也不要把 _collider 改回 clone remap。DynamicCollider 应权威地建立 actor mode 与 non-convex 兼容前置条件;实际 native membership 则只能由 attachShape 的结果确认。_isShapeAttached 仍有必要表达“逻辑 owner 存在但 cooking/native attach 失败”的状态,但必须机械派生自已确认的 native 操作结果。具体删除方向是:删除 DynamicCollider._syncNative() 中偏晚的重复 kinematic 写入,以及 adapter/Core 在未确认 attachShape 成功时的无条件 _shapes.push / _isShapeAttached=true,不要再增加一层同步状态。
已核对为「已解决 / 不适用 / 不重提」
- 旧的“mesh setter 先 attach、collider sync 再 attach”重复挂载问题,本轮通过 owner 收口已解决;本 finding 是相反方向的 native reject + mirror false-positive,不是旧问题降级重提。
- 上轮
CloneModeoverride/default ownership 文案 P2 未被本 delta 触碰,状态不变,不作为本轮增量 finding 重复提出。 - 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction、
DataObject声明等闭环项均未被本 delta 回退。 - 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。
简化建议
修复不需要恢复任何 legacy 路径:把现有 setIsKinematic 前移到 super._syncNative() 之前并删掉原位置,令现有集中式挂载入口只消费 native 的真实结果,再用 native membership/collision 断言替换 JS mirror 断言即可。
GuoLei1990
left a comment
There was a problem hiding this comment.
🫧 尘小沫
增量审查 @ b0f97d14(第四十四轮)— delta = 4 commit(field metadata Symbol 化 + clone hook 显式协议 + metadata 收口)— ❌ 1×P0 未闭环 + 1×P1 新增
总结
自上轮 b10a97c5 起,GitHub compare 为 ahead:4 / behind:0,merge base 正是 b10a97c5,无 force-push。本轮 4 个 commit 修改 70 个文件;CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿。
把字段 metadata 从可碰撞的字符串 _fieldModes 改为私有 Symbol、让 CloneDecorators 成为唯一注册 owner,并删除 CloneManager / CloneMode 过渡层,方向正确;ICloneHook 也正确把 post-clone 协议显式化,并把 identity map 限为只读。但上一轮 native 挂载 P0 的执行顺序和假绿断言逐行未变,同时 hook 改名静默切断了既有用户扩展点。
问题
-
[P0]
packages/core/src/physics/DynamicCollider.ts:487— kinematic triangle-mesh clone 仍在 native actor 切到 kinematic 前挂载 shape;PhysX 拒绝后,adapter 与 Core 仍乐观记录成功,clone 实际没有碰撞形状。ComponentCloner只把_isKinematic=true写进 TS target;DynamicCollider._onClone()随后调用super._onClone(),经Collider._syncNative()在:487先挂载 shape,而 nativesetIsKinematic(this._isKinematic)到:504才执行。PhysX 的attachShape对 non-kinematic dynamic actor + simulation triangle mesh 返回失败,但PhysXCollider.addShape()丢弃返回值并无条件_shapes.push,Collider._setNativeShapeAttached()又无条件写_isShapeAttached=true。测试MeshColliderShape.test.ts:510仍只读取这份乐观 JS mirror,因此继续假绿。请先把 native actor 的 kinematic mode 写入,再执行
super._syncNative();同时让 nativeattachShape结果成为唯一事实 owner:ICollider.addShape返回成功结果,PhysX adapter 只在成功时更新_shapes/ scene shape registration,Core 只据此更新_isShapeAttached。测试应断言真实 Px actor shape 数或 raycast/contact,而不是 adapter mirror。 -
[P1]
packages/core/src/clone/ICloneHook.ts:11— 新协议把文档化的_cloneTo扩展点改名为_onClone,现有用户组件会无编译错误地静默丢失 clone hook。dev/2.0基线文档明确要求“implement the_cloneTo()method”,基线ComponentCloner也实际 duck-call_cloneTo;这不是本 PR 中间态才出现的名字。Git 历史中f1aab7d2 chore: add engine version documentation已把它写入用户文档,886dda635 Fix compont props clone bug (#2926)又保留并集中该 dispatch。目标 HEAD 的CloneUtil/ComponentCloner只调用_onClone,所以已有Script子类即使仍声明_cloneTo(target)也不会报类型错误,只会停止重建 native state、监听或资源 ownership;PR 正文目前还残留多处_cloneTo,也未记录这项 breaking migration。应保留本轮新建的
ICloneHook作为唯一协议 owner,但把其 member 命名为既有的_cloneTo,并机械回改实现、调用、测试和文档链接;不要增加_cloneTo/_onClone双 dispatch 或兼容状态机。这样既获得显式类型与ReadonlyMap边界,也不破坏既有扩展点。
架构治理核对
字段模式链路已正确收口为 CloneDecorators 注册 Symbol metadata → CloneUtil / ComponentCloner 消费,旧 CloneManager、公开 CloneMode 和字符串 metadata 均已删除,没有残留平行 registry。post-clone 行为应继续只由 ICloneHook 定义,但应复用既有方法名,删除本轮多出来的协议改名而不是维护两条路径。
物理链路的权威 owner 必须是 Px actor 的实际 attach 结果;PhysXCollider._shapes 是 scene registration 所需的 confirmed projection,ColliderShape._isShapeAttached 是 Core confirmed cache,两者都不能在 native reject 后自行写成 true。
已核对为「已解决 / 不适用 / 不重提」
- 上轮以前的重复 attach helper / shape backlink 双 ownership 已由
Collider收口,保持解决;本轮 P0 是其下游 native reject 未反馈,不是恢复 shape 侧 helper。 - 上轮
CloneModeoverride/default ownership 文案 P2 随CloneMode整体删除而不再适用。 - metadata 字符串碰撞已由
c28608ed的 Symbol key 与反向测试闭环。 - 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction、
DataObject声明等闭环项均未被本 delta 回退。 - 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。
简化建议
不需要新增同步层:clone 侧用“现有 _cloneTo 名称 + 新 ICloneHook 类型”单协议即可;physics 侧把 kinematic 写入前移,并让现有两层镜像只消费 native attach 的真实结果。
GuoLei1990
left a comment
There was a problem hiding this comment.
🫧 尘小沫
增量审查 @ ebad7d9a(第四十五轮)— delta = 2 commit(native attach 真值闭环 + structural clone 边界加固)— ❌ 2×P1:1 个新数据丢失边界 + 1 个既有扩展协议破坏仍未闭环
总结
自上轮 b0f97d14 起,GitHub compare 为 ahead:2 / behind:0,merge base 正是 b0f97d14,无 force-push;本轮修改 9 个文件(+262/-57)。build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,期间没有新的作者回复、inline comment 或他人 review。
1aa63e94 已正确闭环上一轮物理 P0:native actor 先切 kinematic 再挂 shape,PxRigidActor::attachShape 的失败在 adapter push / scene registration 前抛出,Core 也只在调用成功后写 _isShapeAttached、backlink 和 _shapes;回归测试改为销毁原对象后 raycast 命中 clone,证明了真实 native membership,而不是读取 JS mirror。
ebad7d9a 也修复了 own constructor 字段污染 plain-object 判定及 cross-realm plain object 识别,并让 Date/RegExp 作为直接 @deepClone 值时 honor-or-throw。但新增的“field-cloneable”判据仍不能证明 enumerable field walk 能重建全部状态;同时上一轮指出的 hook 静默改名仍未处理。
问题
-
[P1]
packages/core/src/clone/CloneUtil.ts:323—_canCloneByFields()把[object Object]当成“全部状态可由 enumerable fields 重建”的能力证明,会让@deepClone静默丢失普通用户类的私有状态。这是确定可触发的数据丢失:一个含
#value的用户类默认 tag 就是[object Object]。源对象把#value改为 42 后,当前链路会通过_canCloneByFields,在_createCloneTarget中new ctor()得到默认值 0,再因Object.keys(source)为空而复制不到任何状态;最终cloned !== source,:53的 honor-or-throw 反查也不会抛错,clone 对外读到 0。反方向同样不可靠:一个只有 enumerable state、但自定义Symbol.toStringTag的类会被误判不可 field-clone;新增TaggedCopyValue测试之所以通过,只是因为copyFrom分支在该判据之前早退。Object.prototype.toString只描述 tag,不是 clone capability。请保留 plain/null-prototype record、DataObject与copyFrom作为权威 owner,删除_canCloneByFields这条推断路径;若仍要让@deepClone的软意图进入任意用户 class,应新增一个显式、可验证的 field-clone capability/协议,让该 owner 决定是否 walk,并让无能力的直接装饰值在构造/写字段前抛错。不要再加平台类型白名单或第二份 tag/constructor 启发式。 -
[P1]
packages/core/src/clone/ICloneHook.ts:11— 上轮指出的_cloneTo→_onClone静默 breaking change 仍未闭环;本轮CloneUtil与ComponentCloner仍只 dispatch_onClone。dev/2.0的真实ComponentCloner在:40duck-call_cloneTo,基线用户文档也明确要求实现_cloneTo();已有Script子类不会因这次改名产生编译错误,只会停止重建 native state、监听或资源 ownership。PR 正文至今也仍写着“_cloneTohooks receive(target, cloneMap)”,与目标代码不一致。应保留新
ICloneHook作为 post-clone 协议唯一 owner,但把 member 恢复为既有_cloneTo,并机械回改全部实现、dispatch、测试、文档与 PR 描述;删除本轮引入的_onClone名称,不要维护双 dispatch 或兼容状态机。
架构治理核对
物理链路现已收口为 Px actor 的 attachShape 结果是唯一事实;PhysXCollider._shapes 只是成功 membership 的 scene-registration projection,Core _isShapeAttached / backlink / _shapes 都在成功后机械更新,没有恢复旧的 shape 侧平行 attach 状态机。
clone 链路上,field decorator metadata 仍由 CloneMetadata 唯一写入、CloneUtil / ComponentCloner 消费;当前新增的重复 ownership 是 _canCloneByFields 用 tag 猜测 capability,而 DataObject / copyFrom / plain record 已经各自承担可验证的结构复制协议。应删除 tag 猜测并让显式协议拥有 custom-class cloneability。post-clone 行为则只保留一个 ICloneHook._cloneTo 协议 owner。
已核对为「已解决 / 不适用 / 不重提」
- 上轮 native attach P0 已由
1aa63e94的执行顺序、失败传播和真实 raycast 断言闭环;adapter/Core 不再在 native reject 后写乐观镜像。 - 旧 own-
constructorshadow 与 cross-realm plain-object 判定问题已由 prototype-based_isPlainObject/_createCloneTarget和反向测试闭环;本轮 P1 是新_canCloneByFields无法证明 opaque user state,不是换角度重提 constructor 问题。 - metadata Symbol 化、单一 registry、clone gate、dedup、honor-or-throw、refCount、ShaderData、bare construction、
DataObject声明等历史闭环项未被本 delta 回退。
简化建议
物理侧无需再加同步层。clone 侧最小形态是“plain record + DataObject / copyFrom / 显式 custom capability”决定结构复制,ICloneHook._cloneTo 决定 post-clone 行为;删除 tag 启发式和 _onClone 改名即可,不要维护猜测、兼容与双协议。
There was a problem hiding this comment.
🫧 尘小沫
校正更新 @ e882bae2 — ❌ 1×P1 未闭环 + 1×P2 TSDoc 边界不精确
更正
撤回原 review 中“_cloneTo → _onClone 是 P1 breaking change”的判断。
当前 clone gate 已经拥有自动字段复制的唯一责任,hook 只在字段完成后追加类型行为,因此 _onClone 比 _cloneTo 更准确。新 ICloneHook 也把该协议显式化,并将 identity map 限为只读;目标代码、中英文文档和全部实现已经统一为 _onClone。目标分支是 dev/2.0,旧 ICustomClone._cloneTo 又是 @internal 协议,不应为了旧名称增加双 dispatch 或兼容层。
PR Summary 仍有 5 处 _cloneTo 历史描述,应机械更新为 _onClone;这是非阻塞的描述清理,不再作为 P1。
总结
自本 review 原锚点 00105734 起,后续提交把默认策略收敛为内部 Symbol metadata:Entity / Component → Remap、ReferResource → Assignment、runtime 容器 → Ignore、DataObject → Deep、数学值类型 → Copy。这个方向正确:用户字段零配置进入 Default,类型例外由内部 owner 声明,不公开 @defaultCloneMode。
但新的正向 mode registry 目前只负责 dispatch,没有成为显式 Deep 的 cloneability 边界;CloneUtil 又新增了 Date / RegExp 负向黑名单,因此 opaque state 的静默数据丢失仍未闭环。
问题
-
[P1]
packages/core/src/clone/CloneUtil.ts:111—_isKnownOpaqueObject()只识别Date和RegExp,不能证明其他非普通对象可以由 enumerable field walk 重建。两个确定反例仍会静默损坏:裸
ArrayBuffer不是 view,会进入_cloneObjectByFields(),new ArrayBuffer()只得到 0 字节目标且Object.keys(source)为空;含#value的用户类会通过构造得到默认私有值,但 field walk 无法读取或复制该状态。两种结果都满足cloned !== source,所以 honor-or-throw 不会报错。这轮新增的
defaultCloneModeKey已经提供了正向能力 owner。建议让 plain/null-prototype record、Array/Map/Set/View,以及注册为Copy/Deep的类型进入对应克隆路径;直接@deepClone遇到未注册的非普通 class 时在构造和写字段前抛错,并删除_isKnownOpaqueObject()平台黑名单。不要继续补充 Date/RegExp/URL/Promise 等类型白名单。 -
[P2]
packages/core/src/base/DataObject.ts:4— “objects whose fields are structurally cloned” 仍超过实际契约。执行器只遍历Object.keys(source),即 own enumerable string-keyed properties;private field、non-enumerable property、Symbol key 和 prototype accessor 均不会自动复制。建议改为:
Base class for objects whose own enumerable string-keyed properties are structurally cloned by default.
架构治理核对
CloneDecorators 的 Symbol metadata 现在是默认策略唯一注册 owner,CloneUtil / ComponentCloner 只消费它;该 registry 也应成为结构复制能力的正向边界,避免同时维护一份 opaque 类型黑名单。
post-clone 行为则已经正确收口为 ICloneHook._onClone 单协议:保留新名称,不增加 _cloneTo 兼容路径,只需清理 PR Summary 的旧术语。
当前结论
- P0:0。此前 native shape attach 的执行顺序、失败传播与真实 raycast 验证保持闭环,后续 delta 未触碰物理链路。
- P1:1。仅剩显式 Deep 对 opaque/private state 的静默数据损坏。
- P2:1。
DataObjectTSDoc 未准确陈述Object.keys可见边界。 _onClone:设计合理,原 P1 撤回。
因此 review 继续保持 Changes requested,阻塞项只有上述 cloneability capability boundary。
…t-assignment # Conflicts: # packages/core/src/particle/modules/SizeOverLifetimeModule.ts
GuoLei1990
left a comment
There was a problem hiding this comment.
🫧 尘小沫
增量审查 @ 24017e79(第四十六轮)— delta = 9 commit(3 个 clone follow-up + 1 个 merge + 5 个已进入 dev/2.0 的上游提交)— ❌ 1×P1:普通对象 tag 仍是第二份 cloneability 真相,私有状态继续静默丢失
总结
自上一审查锚点 e882bae2 起,GitHub compare 为 ahead:9 / behind:0,merge base 正是 e882bae2,无 force-push。其中 5 个提交来自最新 dev/2.0;merge commit 没有改 clone 文件。本轮 clone 自身变化是:ccfd52e5 用 branded-object 边界替换 Date/RegExp 黑名单并修正 DataObject TSDoc,766b4762 明确 DataObject/内部 math Copy 两类显式能力并同步文档与测试,24017e79 把只有类内消费者的 helper 改为 private。build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,自上一轮以来没有新的作者回复、inline comment 或他人 review。
DataObject 的 enumerable string-keyed 契约已准确,math copyFrom 也不再靠方法名推断;branded platform objects 作为直接 @deepClone 值会抛错、作为深子树成员会保持 Assignment,均比负向平台类型黑名单更完整。但未注册普通 class 仍由 tag 启发式判为可字段克隆,所以之前的 opaque/private-state P1 只覆盖了 branded 对象,没有闭环。
问题
-
[P1]
packages/core/src/clone/CloneUtil.ts:131—_hasOrdinaryObjectTag()仍把[object Object]当成结构克隆能力,未注册普通类的私有或闭包状态会继续静默损坏。一个确定反例是
class PrivateValue { #value = 0; set value(v) { this.#value = v; } get value() { return this.#value; } }。源值设为 42 后,Object.prototype.toString.call(source)是[object Object],Object.keys(source)是空数组;当前直接@deepClone路径会通过:131,在_createCloneTarget()中new PrivateValue()得到 0,再因没有 enumerable key 而复制不到#value。最终 clone 返回一个新实例且值为 0,既不共享也不抛错。深子树路径在:177使用同一 tag 推断,因此同样会损坏。这不是文档精度问题:本轮已建立
defaultCloneModeKey正向 registry,DataObject → Deep与内部 math 类型→ Copy才是可验证的 class capability owner;_hasOrdinaryObjectTag又维护了一条无法证明对象完整状态可由 field walk 重建的平行能力路径。TaggedCopyValue测试只因人为添加Symbol.toStringTag才被拒绝,不能覆盖同样未注册但无 brand 的普通用户类。请保留 plain/null-prototype record、Array/Map/Set/View,以及注册为
Deep/Copy的类型作为结构复制入口,删除_hasOrdinaryObjectTag()对未注册 class 的能力推断:直接被@deepClone装饰的未注册 class 应在构造和写字段前抛错,深子树里的未注册 class 保持其 Assignment 默认。tests/src/core/Clone.test.ts:780、:1553、:1573目前明确锁定了“未注册 class 也 field-walk”的旧路径;若需要结构语义,应把 fixture 改成DataObject或 plain record,并补一个 private-field 反向测试。不要为这些旧断言保留 compatibility branch、wrapper 或第二条转换路径。
架构治理与熵增核对
上游 writer 是 CloneDecorators 的 Symbol metadata 与 DataObject/math 注册;CloneUtil 应只消费这一能力事实,ComponentCloner 再把结果写入 slot 并结算顶层资源 ownership。相较上一轮,平台黑名单虽已删除,但系统仍是“显式 registry + object tag 推断”两个 class capability owner,owner 数没有真正收敛;新增文档和测试反而把第二条路径固化。删除 tag 推断后,数据流可收口为“plain/container 内建规则,registered class capability,其他 class 默认共享或在直接显式 Deep 时抛错”,无需新增同步状态或 legacy fallback。
24017e79 的 private 化本身是净减法:全仓仅 _cloneObjectFields、_cloneFieldValue、_transferSlotOwnership 有类外消费者,其余 helper 都只有 CloneUtil 内部调用,没有遗留 wrapper。merge 也没有把上游复杂度转移进 clone 链路。
已核对为已解决 / 不适用 / 不重提
DataObjectTSDoc 已由ccfd52e5精确收窄为 own enumerable string-keyed properties,上一轮 P2 闭环。- Date/RegExp/ArrayBuffer/Error/WeakMap 等 branded opaque object 的直接 throw 与嵌套 Assignment 已有正反测试;本 P1 只针对仍被
[object Object]放行的未注册普通 class。 - native shape attach 的执行顺序、失败传播与真实 raycast 验证保持闭环,本 delta 未触碰物理链路。
_onClone是当前单一 post-clone 协议 owner;不恢复_cloneTo,也不增加双 dispatch。PR Summary 已统一新名称。- math Copy 注册、helper private 化和最新
dev/2.0merge 未引入重复 ownership、兼容分支或失效测试。
当前结论
P0:0;P1:1;P2:0。继续 Changes requested,唯一阻塞项是让正向 registry 成为 class cloneability 的唯一 owner,并删除普通 object tag 的推断路径及锁定该旧行为的测试。
Summary
Rework the clone system around a type-driven priority chain, so most objects clone correctly by their type with little to no per-field annotation.
How a field value is cloned — priority high → low
@deepClone/@assignmentClone/@ignoreClone, consulted wherever the clone walks fields (the component top level and inside any object that is itself deep cloned). A decorator is an explicit intent:@deepClonepointed directly at a value that cannot be deep cloned — an Entity/Component reference, an asset, engine runtime state, a function, or an unregistered opaque platform object — throws instead of silently falling back.Entity/Component→ remap through the clone identity map (in-subtree → the corresponding clone; outside → the original reference).ReferResourceassets (Texture / Mesh / Material / Sprite / Font / …) → share.UpdateFlagManager/UpdateFlag→ keep the clone's own (a flag and its manager hold each other; a field copy resolves neither side) —@deepCloneon them throws.DisorderedArray/SafeLoopArray→ keep the clone's own by default; as plain containers,@deepClonemay deep-copy them.Vector*,Matrix*,Color,Ray, …) → copy via their type-ownedcopyFromimplementation. Merely defining a method namedcopyFromdoes not opt a custom type in.DataObjectsubclasses → deep via field walk (27 engine classes: particle modules & curves, shader render states, joint config, PostProcess, ShaderData, Signal, ColliderShape, ui Transition, Skin, …).@deepClonedeep-clones the whole subtreeThe deep intent propagates through containers and field-cloneable ordinary members. Field cloning deliberately covers only own enumerable string-keyed properties; private fields, non-enumerable properties, Symbol keys, and prototype/accessor state are outside the contract. Assets stay shared, entity references remap, runtime state keeps the clone's own, unregistered opaque members keep their assignment default, and nested field decorators still win. Only the value the decorator directly sits on is subject to honor-or-throw.
How a type opts into deep-by-default
A custom structural data type opts in by extending
DataObject(public marker base class). Engine math value types are registered internally with a Symbol-backedCopydefault because math cannot depend on core; a user-definedcopyFrommethod alone is not an opt-in. Copy/Deep types require argument-less construction when no compatible preset exists: the gate bare-constructs the target and then populates it, and reports a named error if that fails.Ref-count model — slot-ownership contract
ReferResourceowns one reference: the gate acquires it while cloning the slot (+1, and−1when displacing an owned constructor preset —_transferSlotOwnership). Releasing it on destroy is the owning class's responsibility.Transition._onClone+1 ↔Transition.destroy()−1 (all four state slots in the base class);ShaderData.cloneTocascades by the host's refCount, texture-array entries included.@ignoreCloneso the setter is the single+1source (MeshRenderer.mesh,Renderermaterials,MeshColliderShape.mesh, particleMeshShape.mesh).destroy().Changes
CloneDecoratorsis the single owner of the internal Symbol metadata and registrations; execution lives inCloneUtil. Public clone API = the three field decorators +DataObject; default modes and their Symbol keys stay internal.Signal._listeners,MeshColliderShape._mesh).@shallowCloneremoved (breaking) — migrate with@deepClone/@assignmentClone.CloneUtilsand thesrcRoot/targetRootpath-walk remap removed — Entity/Component remap unified through the clone identity map;_onClonehooks receive(target, cloneMap).@assignmentClone/@ignoreCloneon Entity/Component refs are honored literally (share / keep own) instead of being silently overridden by auto-remap;@deepCloneon an unhonorable value throws instead of warning and falling back.Object.keys(own keys) instead offor...in: the loose ES5 bundle makes prototype methods enumerable, sofor...incopied them onto every clone as own properties — clones now match a fresh instance's shape (MeshRenderer 61 → 31 own keys), and a 20-entity tree clones 4× faster (0.39 → 0.09 ms)._onClonehooks running exactly once per source object are all pinned by tests.ShaderDatajoins the deep family with a_onClonehook: a renderer's shaderData is now actually cloned — previously all of its fields were@ignoreClonewith no hook, so renderer clones silently lost floats and macros. Texture-array entries cascade through_addReferCountlike single textures (review 4629713846).CustomDataModuledrops its hand-written_onClone— the container defaults reproduce it, ShaderProperty identity preserved.MeshColliderShapeclones cook a native shape through the mesh setter and attach exactly once (_isShapeAttachedbookkeeping hoisted toColliderShapeforCollider's generic attach/detach path).docs/en|zh/core/clone.mdx, how-to-contribute) rewritten to the final semantics: default-rules table (functions and Map keys included), the publicDataObjectopt-in and internally registered math Copy types, the enumerable string-field contract, subtree propagation, ref-count contract, and@shallowClonemigration callout.Pre-existing imbalances fixed alongside (exposed by the ref-count rework)
TextRenderer._font/ uiText._font(destroy released a count the clone never acquired) andMeshColliderShape._mesh(same, plus the cloned shape silently lacked a native shape) were unbalanced ondev/2.0before this PR; the slot-ownership contract requires them fixed here to stay self-consistent. Destroying aParticleRendereralso never released aMeshShape's mesh — fixed via aBaseShape._destroyhook on the generator destroy chain.Test plan
dev/2.0: 1653 tests / 122 files pass (core / ui / loader / math / PhysX physics);pnpm run b:allalso passes._onCloneexactly-once, binary preset reuse, Map key cloning, template-source countingMerge 决策记录 —
7ba6d16f3(dev/2.0 同步合并)本次同步合并在文本冲突解决之外,有意修改了三处上游代码行(可通过
git show 7ba6d16f3的 combined diff 查看)。三处删除在类型驱动闸门下均行为等价:Animator.ts— 删除 fix(animation): harden animator state instance data #3024 新增的fireEvents上的@assignmentClone。 该装饰器进入本分支时处于"使用但未导入"状态(本分支已删除该文件的对应 import),不删会在 shader-compiler 预编译时抛ReferenceError;布尔原始值在闸门下天然按值拷贝,装饰器冗余。VelocityOverLifetimeModule.ts— 删除 feat(particle): add orbital radial velocity over lifetime #3049 新增的 5 个@deepClone(orbital 曲线 ×4 +_offset)。ParticleCompositeCurve与Vector3经类型默认即深拷,与同文件已去装饰器的_velocityX/Y/Z口径一致。由测试 "orbital velocity fields deep-clone through the type default" 钉住。ParticleCompositeCurve.ts— 删除上游为_updateManager补的@ignoreClone。UpdateFlagManager已按类型默认 Ignore,字段装饰器冗余。🤖 Generated with Claude Code